//
//  CTVSync.m
//  Channltv
//
//  Created by Bruno Keymolen on 7/5/07.
//  Copyright 2007 channl.tv. All rights reserved.
//
//   State Machine: 
//
//   CTVSyncState_NotReady --> CTVSyncState_Idle --> CTVSyncState_ChannelCollection --> CTVSyncState_ChannelDetail
//                                    ^                          |                                  |
//                                    |                          v                                  v
//                                    +--------------------------+----------------------------------+
//
//




#import "CTV.h"
#import "CTVSync.h"
#import "CTVHTTPDataDownload.h"
#import "CTVChannelCollectionParser.h"
#import "CTVClipInChannelParser.h"
#import "CTVClip.h"
#import "CTVSystemFeedback.h"
#import "CTVUtil.h"

@implementation CTVSync


+(id)GetInstance {
	static id _CTVSingleton = nil;
	if (_CTVSingleton == nil) {
		_CTVSingleton = [self new];
		[_CTVSingleton setup];
	}	
    return _CTVSingleton;
}


-(void)setup{
	m_listeners = [[NSMutableArray alloc] init];
	m_config = [CTVConfig GetInstance];
	m_syncData		= [[NSMutableArray alloc] init];
	m_syncMedia		= [[NSMutableArray alloc] init];
	m_syncResources = [[NSMutableArray alloc] init];
	m_inSyncURLs    = [[NSMutableDictionary alloc] init];
	m_syncState = CTVSyncState_Idle;
	
	[[CTVCache GetInstance] addCacheListener:(CTVCacheListener*)self];

	
}

-(BOOL)inSync{
	if(m_syncState == CTVSyncState_Idle){
		return FALSE;
	}
	return TRUE;
}

-(void)addListener:(CTVSyncListener*)listener{
	[m_listeners addObject:listener];
}

-(void)removeListener:(CTVSyncListener*)listener{
	[m_listeners removeObject:listener];
}



//1. Download Data to chache (overwrite and remove old content) -> async
//2. Download Resource (never overwrite and remove old content) -> async
//3. Download Media (never overwrite and remove old content)	-> async
-(int)syncWithNetwork{
	int ret;
	
	//We only sync if we are in CTVSyncState_Idle
	if(m_syncState != CTVSyncState_Idle){
		return -1;
	}	
	
	[self _gotoState:CTVSyncState_ChannelCollection];
	
	
	
	
	m_syncLastError = 0;
	

	//Login into channl.tv
	NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
	CTVUser* activeUser =[m_config activeUser];
	ret = [activeUser loginToChannelTVByAlias:[defaults objectForKey:CTVCONF_USERNAME] andPassword:[defaults objectForKey:CTVCONF_PASSWORD]];
	if(ret != 0){
		m_syncLastError = -902;
		[self _gotoState:CTVSyncState_Idle];		
		return -1; //Could not logon 
	}
	
	//Clean up to get ready
	[m_syncData removeAllObjects];
	[m_syncResources removeAllObjects];
	[m_syncMedia removeAllObjects];
	[m_inSyncURLs removeAllObjects];	

	NSString* rootURL;	
	NSString* dataCachePath;
	NSEnumerator* rootURLS; 
	
	//First store the URL's in a collection (m_inSyncURLs), if the URL's are all processed, the state machine can goto the next step
	rootURLS = [[m_config dataRootURLs] objectEnumerator]; 
	while (rootURL = [rootURLS nextObject]) {
		[m_inSyncURLs setValue:@"channel" forKey:rootURL];
	}
	
	//Download the URL's, if done (or error) the CTVCache callback is called which will remove the urls from the m_inSyncURLs collection	
	rootURLS = [[m_config dataRootURLs] objectEnumerator]; 
	while (rootURL = [rootURLS nextObject]) {
		//download the channel, if error : skip
		dataCachePath = [[CTVCache GetInstance] cacheData:rootURL];
		if(dataCachePath != nil){
			[m_syncData addObject:dataCachePath]; //Store hash url, for sync feedback (ex: url is in sync)
		}
	}
	
	return 0;
}




/* Sync channels from cache
 * Keep existing channels in memory (at the same memory location) to avoid "menu resets" after or during sync
 */
-(int)syncChannelsFromDisk{	
	NSEnumerator* rootUrls = [[[CTVConfig GetInstance] dataRootURLs] objectEnumerator] ;
	NSString* urlString;
	NSMutableDictionary* rootChannels = [[CTV GetInstance] rootChannels]; 
	CTVRootChannel* rootChannel;
	CTVChannel* channel;
	CTVChannel* parsedChannel;
	NSEnumerator* channels;
	NSString* key;
	NSEnumerator* clips;
	CTVClip* clip;

	while(urlString = [rootUrls nextObject]){
		//Get cache entry for the channel(s) file
		NSString* cacheEntry = [CTVCache getDataCachePathForURL:urlString];
	
		//Is the rootchannel in cache ?
		if( [[NSFileManager defaultManager] fileExistsAtPath:cacheEntry] ){

	
			//Does this rootchannel already exist?
			rootChannel = [rootChannels objectForKey:urlString];
			//If no, add
			if(rootChannel == nil){
				rootChannel = [[CTVRootChannel alloc] init];
				[rootChannels setObject:rootChannel forKey:urlString];
			}else{
			//Yes, flag all channels willBeDeleted=1, this to allow us to clean up the channel after sync
				channels = [[rootChannel channels] objectEnumerator];
				while(channel = [channels nextObject]){
					[channel setWillBeDeleted:TRUE];
				} 
			}
			
						
			//Parse and Add
			CTVChannelCollectionParser* channelParser = [[CTVChannelCollectionParser alloc] init];
			int ret = [channelParser parseFromPath:cacheEntry];
			if(ret == 0){
				/* Update all channels wich are found by the parser */
				[rootChannel setName:[channelParser channelCollectionName]];
				channels = [[channelParser channels] objectEnumerator]; 
				while (parsedChannel = [channels nextObject]) {
					//Do we have this channel already in our rootChannel ?
					channel = [[rootChannel channels] objectForKey:[parsedChannel url]];
					if(channel == nil){
						[parsedChannel retain];	
						channel = parsedChannel;	
						[[rootChannel channels] setObject:channel forKey:[channel url]];
					}else{
						[channel setThumbUrl:[parsedChannel thumbUrl]];
						[channel setName:[parsedChannel name]];
						[channel setChannelId:[parsedChannel channelID]];
					}
					//Remember a pointer to the root channel
					[channel setRootChannelPointer:rootChannel];
					
					//This channel is still in our configuration
					[channel setWillBeDeleted:FALSE];
					
					//Remove all clips from this channel (@TODO:Sync clips, don't remove & build)
					[channel removeAllPrograms]; 
					
					//Load and add the clips for this channel 
					cacheEntry = [CTVCache getDataCachePathForURL:[channel url]];
					CTVClipInChannelParser* clipInChannelParser = [[CTVClipInChannelParser alloc] init];
					
		if(	[[channel name] compare:@"Het journaal 13u"] == NSOrderedSame || [[channel name] compare:@"Het journaal 19u"] == NSOrderedSame ){		
					[clipInChannelParser parseFromPath:cacheEntry orderNewestFirst:FALSE];
		}else{			
					[clipInChannelParser parseFromPath:cacheEntry orderNewestFirst:TRUE];
		}
					
					clips = [[clipInChannelParser clips] objectEnumerator];
					while (clip = [clips nextObject]) {
						[clip retain];
						[clip setChannelPointer:channel];
						[channel addProgram:clip];
					}			
					[clipInChannelParser release];	

					//Initialize watchpoint / if not done
					if([[channel watchPoint] initialized] == FALSE){
						[channel resetWatchPoint];
					}
					[[CTVCache GetInstance] loadWatchPoint:channel];
NSLog(@"CTVSync watchpoint offset=%f, clip=%@<", [[channel watchPoint] offset], [[channel watchPoint] clipID] );			

					
				}
			}
			[channelParser release];	
		
		
			//Inform our liseners if channels are updated (ex. menu's can redraw etc)
			NSEnumerator* listeners;
			CTVSyncListener* listener;
			listeners = [m_listeners objectEnumerator];
			while(listener = [listeners nextObject]){
				[listener syncWillFinish];
			}	
		
			//Loop through our collection, all channels without a flag "willBeDeleted" are deleted now (!)
			channels = [[rootChannel channels] keyEnumerator];
			while(key = [channels nextObject]){
				channel = [[rootChannel channels] objectForKey:key];
				if([channel willBeDeleted] == TRUE){
					[[rootChannel channels] removeObjectForKey:key];
					[channel release];
				}
			}
		} //Is the rootchannel in cache ?			
				
	} //While RootChannels
	
	
	return 0;	
}





//CTVCacheListener
-(void)cacheFinishedForURL:(NSString*)url andStatusCode:(int)statusCode{

	//get the type
	NSString* type = [m_inSyncURLs objectForKey:url];
		
	if(type == nil){
		//Not a URL we are interested in
		return;
	}		
					
	//If URL is channel -> parse channel from cache
	if([type isEqualToString:@"channel"]){
		//If error; stop (statuscode < 0 : system error, >= 400 : HTTP Error)
		if(statusCode >= 0 && statusCode < 400){ 
			[self _parseAndHandleChannel:url];
		}else{
			//Set a sync error
			m_syncLastError = statusCode;
			
			//Inform the channl.tv server
			NSString* msg = [NSString stringWithFormat:@"Error; Download System XML; Status code:%d ; URL=%@", statusCode, url];
			[CTVSystemFeedback systemFeedBackWithMessage:msg];			
		}
	}
	
	//If URL is clipsinchannel -> parse clipsinchannel from cache
	if([type isEqualToString:@"clipsinchannel"]){
		if(statusCode >= 0 && statusCode < 400){ 
			[self _parseAndHandleClipsInChannel:url];
		}else{
			//Set a sync error
			m_syncLastError = statusCode;

			//Inform the channl.tv server
			NSString* msg = [NSString stringWithFormat:@"Error; Download System XML; Status code:%d ; URL=%@", statusCode, url];
			[CTVSystemFeedback systemFeedBackWithMessage:msg];
		}
	}

	//Remove URL from our collection, status is "not in sync"	
	[m_inSyncURLs removeObjectForKey:url];
	
	//if collection is empty, we are done -> send sync ready to the listeners
	if([m_inSyncURLs count] == 0){
		//Update the channels and clips tree
		[self syncChannelsFromDisk];
		//Clean up the disk (remove files which are not in our collection any more)
		[self _syncDiskFiles];
		//Goto idle state, Send sync finish to listeners
		[self _gotoState:CTVSyncState_Idle];
	}	

}


//Private
-(void)_gotoState:(CTVSyncState)newState{
	NSEnumerator* listeners;
	CTVSyncListener* listener;
	CTVSyncState prevState;
	
	prevState = m_syncState;
	
	m_syncState = newState; //Must be set first because the listeners can/will query the status
	
	//CTVSyncState_Idle -> other : send "syncStarted"
	if(prevState == CTVSyncState_Idle && newState != CTVSyncState_Idle){
		listeners = [m_listeners objectEnumerator];
		while(listener = [listeners nextObject]){
			[listener syncStarted];
		}	
	}
	//other -> CTVSyncState_Idle : send "syncStopped"
	if(prevState != CTVSyncState_Idle && newState == CTVSyncState_Idle){
		
		
		listeners = [m_listeners objectEnumerator];
		while(listener = [listeners nextObject]){
			[listener syncStopped];
		}	
	}
	 
}




-(void)_parseAndHandleChannel:(NSString*)url{
	CTVChannel* channel;
	int ret;
	NSString* resourceCachePath;
	NSString* dataCachePath;
	
	[self _gotoState:CTVSyncState_ChannelDetail];

	CTVChannelCollectionParser* channelParser = [[CTVChannelCollectionParser alloc] init];
	ret = [channelParser parseFromPath: [CTVCache getDataCachePathForURL:url] ];
	if(ret == 0){
		//We have the channels out of this file, check now the clipsInChannel url for all channels
		NSEnumerator* channelsEnum = [[channelParser channels] objectEnumerator]; 
		while (channel = [channelsEnum nextObject]) {					
			//Store the URL in our "in-sync" collection
			[m_inSyncURLs setValue:@"thumb" forKey:[channel thumbUrl]];  
			//Start downloading the resource (ASYNC(!))
			resourceCachePath = [[CTVCache GetInstance] cacheResource:[channel thumbUrl]];
			if(resourceCachePath != nil){
				//Store the channel thumbnail URL for file - 2 way - sync
				[m_syncResources addObject:resourceCachePath]; 
			}
			//Store the URL in our "in-sync" collection
			[m_inSyncURLs setValue:@"clipsinchannel" forKey:[channel url]];  
			//Get the clipsInChannel XML
			dataCachePath = [[CTVCache GetInstance] cacheData:[channel url] ]; //url = clipsInChannel URL
			if(dataCachePath != nil){
				[m_syncData addObject:dataCachePath]; //Store hash url, for sync
			}
		}
	}
	[channelParser release];
}


-(void)_parseAndHandleClipsInChannel:(NSString*)url{
	int ret;
	NSString* resourceCachePath;
	NSString* mediaCachePath;

	[self _gotoState:CTVSyncState_ChannelDetail];

	//Parse
	CTVClipInChannelParser* clipParser = [[CTVClipInChannelParser alloc] init];

//	if(	[[channel name] compare:@"Het journaal 13u"] == NSOrderedSame){		
//		ret = [clipParser parseFromPath:[CTVCache getDataCachePathForURL:url] orderNewestFirst:FALSE];
//	}else{			
		ret = [clipParser parseFromPath:[CTVCache getDataCachePathForURL:url] orderNewestFirst:TRUE];
//	}
			
	
	if(ret == 0){
		NSEnumerator* clipsEnum = [[clipParser clips] objectEnumerator]; 
		CTVClip* clip;	
		while (clip = [clipsEnum nextObject]) {
			//Store the URL in our "in-sync" collection
			[m_inSyncURLs setValue:@"thumb" forKey:[clip getThumbUrl]];  
			//Start downloading the resource (ASYNC(!))
			resourceCachePath = [[CTVCache GetInstance] cacheResource:[clip getThumbUrl]];
			if(resourceCachePath != nil){
				//Store the channel thumb URL for file - 2 way - sync
				[m_syncResources addObject:resourceCachePath]; 
			}

			//Start downloading the clip if flag "sync==TRUE" (ASYNC(!))
			if([clip autoSyncMedia] == TRUE){
				mediaCachePath = [[CTVCache GetInstance] cacheMedia:[clip getUrl] andClip:clip];
				if(mediaCachePath != nil){
					//Store the chip URL
					[m_syncMedia addObject:mediaCachePath]; //Store hash url, for 2 way sync
				}
			}else{
				//Store the clip URL
				[m_syncMedia addObject:[CTVCache getMediaCachePathForURL:[clip getUrl]] ]; //Store hash url, for sync								
			}
		}							
	}
	[clipParser release];

}


-(int)syncLastError{
	return m_syncLastError;
}

// 2 way sync 
// remove all obsolete files in the cache
-(void)_syncDiskFiles{
	NSString* urlString;
	NSString* hash;
	NSString* cmd;
	NSEnumerator* urlEnum;
	NSEnumerator* tmpEnum;
	NSString* tmpString;
	NSMutableArray* tmpArray;
	
	NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
	if(![[defaults objectForKey:CTVCONF_REMOVEOBSOLETECLIPS] isEqualToString:@"Yes"] ){
		return; 
	}
	
	
	NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

	tmpArray =  [NSMutableArray arrayWithCapacity:5];  
	
	//Get all cache entry's
	//Search the URL's, if found, remove from cache entry list
	//Remove all entry's still in list

	//Data
	NSMutableArray* dataFiles =  [NSMutableArray arrayWithCapacity:100];  
	[dataFiles addObjectsFromArray:[[NSFileManager defaultManager] directoryContentsAtPath:[[CTVConfig GetInstance] dataCacheDir]] ];
	urlEnum = [m_syncData objectEnumerator]; 
	while (urlString = [urlEnum nextObject]){
		//Cut the hash
		int slash = [CTVUtil getLastOccurenceOfChar:'/' inString:urlString];
		if(slash > 0){
			hash = [urlString substringFromIndex:slash+1];
		}else{
			hash = urlString;
		}	
		[dataFiles removeObject:hash]; //A string can be removed in this way

	}
	
	//Resources
	NSMutableArray* resourceFiles =  [NSMutableArray arrayWithCapacity:100];  
	[resourceFiles addObjectsFromArray:[[NSFileManager defaultManager] directoryContentsAtPath:[[CTVConfig GetInstance] resourceCacheDir]] ];
	urlEnum = [m_syncResources objectEnumerator]; 
	while (urlString = [urlEnum nextObject]) {
		//Cut the hash
		int slash = [CTVUtil getLastOccurenceOfChar:'/' inString:urlString];
		if(slash > 0){
			hash = [urlString substringFromIndex:slash+1];
		}else{
			hash = urlString;
		}	
		[resourceFiles removeObject:hash]; //A string can be removed in this way

	}
	
	//Media
	NSMutableArray* mediaFiles =  [NSMutableArray arrayWithCapacity:100];  
	[mediaFiles addObjectsFromArray:[[NSFileManager defaultManager] directoryContentsAtPath:[[CTVConfig GetInstance] mediaCacheDir]] ];
	urlEnum = [m_syncMedia objectEnumerator]; 
	while (urlString = [urlEnum nextObject]) {
		//Cut the hash
		int slash = [CTVUtil getLastOccurenceOfChar:'/' inString:urlString];
		if(slash > 0){
			hash = [urlString substringFromIndex:slash+1];
		}else{
			hash = urlString;
		}
		//Get all mediaFiles entry's belonging to this hash	
		[tmpArray removeAllObjects];
		tmpEnum = [mediaFiles objectEnumerator];
		while (tmpString = [tmpEnum nextObject]) {
			if([tmpString hasPrefix:hash]){ 
				[tmpArray addObject:tmpString];
			}	
		}
		//Remove all mediafiles belong to the hash	
		tmpEnum = [tmpArray objectEnumerator];
		while (tmpString = [tmpEnum nextObject]) {
			[mediaFiles removeObject:tmpString];
		}
	}

 
	//Remove Data
	urlEnum = [dataFiles objectEnumerator]; 
	while (urlString = [urlEnum nextObject]){
		cmd = [NSString stringWithFormat:@"rm -f %@%@", [[CTVConfig GetInstance] dataCacheDir], urlString];
		system([cmd UTF8String]);
		NSLog(@"%@", cmd);
	}
	
	//Remove Resources
	urlEnum = [resourceFiles objectEnumerator]; 
	while (urlString = [urlEnum nextObject]){
		cmd = [NSString stringWithFormat:@"rm -f %@%@", [[CTVConfig GetInstance] resourceCacheDir], urlString];
		system([cmd UTF8String]);
		NSLog(@"%@", cmd);
	}
	
	//Remove Media
	urlEnum = [mediaFiles objectEnumerator]; 
	while (urlString = [urlEnum nextObject]){
		cmd = [NSString stringWithFormat:@"rm -f %@%@", [[CTVConfig GetInstance] mediaCacheDir], urlString];
		if([urlString hasPrefix:@"_"]){
			//Downloading
		}else{
			system([cmd UTF8String]);
			NSLog(@"%@", cmd);
		}
	}


	[dataFiles removeAllObjects];
	[resourceFiles removeAllObjects];
	[mediaFiles removeAllObjects];
	[tmpArray removeAllObjects];
	
	[pool release];
}


@end


